﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Graphics.Implementation;
using Tesla.Math;
using Tesla.Util;
using D3D = SlimDX.Direct3D10;
using SDX = SlimDX;

namespace Tesla.Direct3D10.Graphics.Implementation {
    /// <summary>
    /// Concrete Direct3D10 implementation of <see cref="Texture1D"/>.
    /// </summary>
    public sealed class D3D10Texture1DImplementation : Texture1DImplementation {
        private D3D10Renderer _renderer;
        private D3D.Device _graphicsDevice;
        private D3D.ShaderResourceView _shaderResourceView;
        private D3D.Texture1D _texture1D;
        private D3D.Texture1D _staging;
        private D3D.ResourceUsage _usage;
        private int _mipCount;

        /// <summary>
        /// Gets the D3D10Renderer that created this implementation.
        /// </summary>
        internal D3D10Renderer Renderer {
            get {
                return _renderer;
            }
        }

        /// <summary>
        /// Gets the D3D10 ShaderResourceView.
        /// </summary>
        internal D3D.ShaderResourceView D3D10ShaderResourceView {
            get {
                return _shaderResourceView;
            }
        }

        /// <summary>
        /// Gets the D3D10 Texture1D.
        /// </summary>
        internal D3D.Texture1D D3DTexture1D {
            get {
                return _texture1D;
            }
        }

        /// <summary>
        /// Gets the number of mip map levels in the texture resource.
        /// </summary>
        public override int MipCount {
            get {
                return _mipCount;
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10Texture1DImplementation"/>.
        /// </summary>
        /// <param name="renderer">The D3D10 renderer.</param>
        /// <param name="width">The width of the texture.</param>
        /// <param name="genMipMaps">True if mip maps should be generated.</param>
        /// <param name="format">The surface format.</param>
        /// <param name="data">The initial data.</param>
        internal D3D10Texture1DImplementation(D3D10Renderer renderer, int width, bool genMipMaps, SurfaceFormat format, DataBuffer data) 
            : base(width, format) {
            //Set common properties
            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;

            //Do we want to generate mip maps, and do we have the data to do so? (if not, cancel mip map generation)
            bool canGenMipmaps = (genMipMaps && (data != null));

            D3D.Texture1DDescription descTex = new D3D.Texture1DDescription();
            descTex.ArraySize = 1;
            descTex.Width = width;
            descTex.Usage = _usage = D3D.ResourceUsage.Default;
            descTex.CpuAccessFlags = D3D.CpuAccessFlags.None;
            descTex.Format = D3D10Helper.ToD3DSurfaceFormat(format);
            descTex.MipLevels = (genMipMaps) ? 0 : 1;

            //Set mip map generation params
            if(canGenMipmaps) {
                descTex.BindFlags = D3D.BindFlags.ShaderResource | D3D.BindFlags.RenderTarget;
                descTex.OptionFlags = D3D.ResourceOptionFlags.GenerateMipMaps;
            } else {
                descTex.BindFlags = D3D.BindFlags.ShaderResource;
                descTex.OptionFlags = D3D.ResourceOptionFlags.None;
            }

            //Create the texture and shader view
            _texture1D = new D3D.Texture1D(_graphicsDevice, descTex);
            _shaderResourceView = new D3D.ShaderResourceView(_graphicsDevice, _texture1D);

            //Add to tracker
            _renderer.Resources.AddTrackedObject(_texture1D.ComPointer, this);
            _renderer.Resources.AddTrackedObject(_shaderResourceView.ComPointer, this);

            //Set the final mip count
            _mipCount = _texture1D.Description.MipLevels;

            //Now set the initial data if its present
            if(data != null) {
                try {
                    this.SetData<byte>(data.ByteDataCopy, 0, 0, width, 0, data.ElementSizeInBytes * data.Length);

                    if(genMipMaps) {
                        _graphicsDevice.GenerateMips(_shaderResourceView);
                    }

                    //Dispose of the staging texture
                    if(_staging != null) {
                        _staging.Dispose();
                        _staging = null;
                    }
                } catch(Exception e) {
                    Dispose();
                    throw new TeslaException("Error setting Texture data.", e);
                }
            }
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="D3D10Texture1DImplementation"/> is reclaimed by garbage collection.
        /// </summary>
        ~D3D10Texture1DImplementation() {
            Dispose(false);
        }

        /// <summary>
        /// Gets the data from the texture.
        /// </summary>
        /// <typeparam name="T">Type of data in the array.</typeparam>
        /// <param name="data">Array of data</param>
        /// <param name="mipLevel">Mip map level to access</param>
        /// <param name="left">The left-most position in the 1D texture at which to access.</param>
        /// <param name="right">The right-most position in the 1D texture at which to acess.</param>
        /// <param name="startIndex">Starting index in the array to start writing to.</param>
        /// <param name="elementCount">Number of elements to read.</param>
        public override void GetData<T>(T[] data, int mipLevel, int left, int right, int startIndex, int elementCount) {
            if(_texture1D.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            if(mipLevel < 0 || mipLevel >= _mipCount) {
                throw new ArgumentOutOfRangeException("mipLevel", String.Format("Mip level is out of range. Must be between 0 and {0}.", _mipCount.ToString()));
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);
            SurfaceFormat format = base.Format;


            int formatSize = D3D10Helper.FormatSize(format);
            int elemSize = MemoryHelper.SizeOf<T>();
            CheckSizes(formatSize, elemSize);

            //Calc subresource dimensions 
            int width = (int) MathHelper.Max(1, base.Width >> mipLevel);

            //Check the line
            CheckLine(ref width, left, right);

            //Check the total size - fixes up the proper sizes if we're using a compression format
            CheckTotalSize(format, ref formatSize, ref width, elemSize, elementCount);

            //Create staging
            CreateStaging();

            try {
                //Copy from the texture to staging
                if(format == SurfaceFormat.DXT1 || format == SurfaceFormat.DXT3 || format == SurfaceFormat.DXT5) {
                     _graphicsDevice.CopyResource(_texture1D, _staging);
                } else {
                    D3D.ResourceRegion region = new D3D.ResourceRegion();
                    region.Left = left;
                    region.Right = right;
                    region.Top = 0;
                    region.Bottom = 1;
                    region.Front = 0;
                    region.Back = 1;
                    _graphicsDevice.CopySubresourceRegion(_texture1D, mipLevel, region, _staging, mipLevel, left, 0, 0);
                }
                SDX.DataStream ds = _staging.Map(mipLevel, D3D.MapMode.Read, D3D.MapFlags.None);

                //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));

                //Read data
                ds.Position = left * formatSize;
                ds.ReadRange<T>(data, startIndex, actWidth);

                _staging.Unmap(mipLevel);
            } catch(Exception e) {
                throw new TeslaException("Error reading from D3D10 Texture2D.", e);
            }
        }

        /// <summary>
        /// Sets the data from the array to the texture.
        /// </summary>
        /// <typeparam name="T">Type of data in the array.</typeparam>
        /// <param name="data">Array of data</param>
        /// <param name="mipLevel">Mip map level to access</param>
        /// <param name="left">The left-most position in the 1D texture at which to access.</param>
        /// <param name="right">The right-most position in the 1D texture at which to acess.</param>
        /// <param name="startIndex">Starting index in the array to start reading from.</param>
        /// <param name="elementCount">Number of elements to write.</param>
        public override void SetData<T>(T[] data, int mipLevel, int left, int right, int startIndex, int elementCount) {
            if(_texture1D.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            if(mipLevel < 0 || mipLevel >= _mipCount) {
                throw new ArgumentOutOfRangeException("mipLevel", String.Format("Mip level is out of range. Must be between 0 and {0}.", _mipCount.ToString()));
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);
            SurfaceFormat format = base.Format;

            int formatSize = D3D10Helper.FormatSize(format);
            int elemSize = MemoryHelper.SizeOf<T>();
            CheckSizes(formatSize, elemSize);

            //Calc subresource dimensions 
            int width = (int) MathHelper.Max(1, base.Width >> mipLevel);

            //Check the line
            CheckLine(ref width, left, right);

            CheckTotalSize(format, ref formatSize, ref width, elemSize, elementCount);

            //Create staging
            if(_usage == D3D.ResourceUsage.Default) {
                CreateStaging();
            }

            try {
                if(_usage == D3D.ResourceUsage.Default) {
                    SDX.DataStream ds = _staging.Map(mipLevel, D3D.MapMode.Write, D3D.MapFlags.None);

                    //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                    int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));

                    //Read data
                    ds.Position = left * formatSize;
                    ds.WriteRange<T>(data, startIndex, actWidth);
                    _staging.Unmap(mipLevel);

                    //Copy from the staging to texture
                    if(format == SurfaceFormat.DXT1 || format == SurfaceFormat.DXT3 || format == SurfaceFormat.DXT5) {
                         _graphicsDevice.CopyResource(_staging, _texture1D);
                    } else {
                        D3D.ResourceRegion region = new D3D.ResourceRegion();
                        region.Left = left;
                        region.Right = right;
                        region.Top = 0;
                        region.Bottom = 1;
                        region.Front = 0;
                        region.Back = 1;
                        _graphicsDevice.CopySubresourceRegion(_staging, mipLevel, region, _texture1D, mipLevel, left, 0, 0);
                    }
                } else {
                    SDX.DataStream ds = _texture1D.Map(mipLevel, D3D.MapMode.WriteDiscard, D3D.MapFlags.None);

                    //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                    int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));

                    //Read data
                    ds.Position = left * formatSize;
                    ds.WriteRange<T>(data, startIndex, actWidth);
                    _texture1D.Unmap(mipLevel);
                }
            } catch(Exception e) {
                throw new TeslaException("Error writing to D3D10 Texture1D.", e);
            }
        }

        private static void CheckSizes(int formatSize, int elementSize) {
            if(formatSize != elementSize) {
                if((formatSize < elementSize) || ((formatSize % elementSize) != 0)) {
                    throw new ArgumentException("Element format size does not match texture's expected format size.");
                }
            }
        }

        private static void CheckLine(ref int width, int left, int right) {
            if(left < 0 || right <= 0) {
                throw new ArgumentOutOfRangeException("Left cannot be less than zero and right cannot be less than or equal to zero.");
            }

            if(left > width || right > width || left == right || width < 0) {
                throw new ArgumentOutOfRangeException("Invalid subimage left/right values.");
            }

            width = right - left;
        }

        private static void CheckTotalSize(SurfaceFormat format, ref int formatSize, ref int width, int elementSize, int elementCount) {
            //1D textures don't support DXT compression apparently
            //if(format == SurfaceFormat.DXT1 || format == SurfaceFormat.DXT3 || format == SurfaceFormat.DXT5) {
            //    width = (width + 3) >> 2;
            //    formatSize = (format == SurfaceFormat.DXT1) ? 8 : 16;
            //}

            int totalSize = width * formatSize;
            if(elementSize * elementCount != totalSize) {
                throw new ArgumentException("Total size of data to write does not match expected total size.");
            }
        }

        private void CreateStaging() {
            if(_staging == null) {
                D3D.Texture1DDescription desc = new D3D.Texture1DDescription();
                desc.ArraySize = 1;
                desc.BindFlags = D3D.BindFlags.None;
                desc.CpuAccessFlags = D3D.CpuAccessFlags.Write | D3D.CpuAccessFlags.Read;
                desc.Format = D3D10Helper.ToD3DSurfaceFormat(base.Format);
                desc.Width = base.Width;
                desc.MipLevels = _mipCount;
                desc.Usage = D3D.ResourceUsage.Staging;

                _staging = new D3D.Texture1D(_graphicsDevice, desc);

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_staging.ComPointer, this);
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing) {
            if(!IsDisposed) {
                //Dispose of managed resources
                if(disposing) {
                    if(_shaderResourceView != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_shaderResourceView.ComPointer);
                        }
                        _shaderResourceView.Dispose();
                        _shaderResourceView = null;
                    }

                    if(_texture1D != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_texture1D.ComPointer);
                        }
                        _texture1D.Dispose();
                        _texture1D = null;
                    }

                    if(_staging != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_staging.ComPointer);
                        }
                        _staging.Dispose();
                        _staging = null;
                    }
                }
                base.Dispose(disposing);
            }
        }
    }
}
